#!/usr/bin/env python3

import imp
import os
import signal
import subprocess
import sys

import common
from shell_helpers import LF

class GdbTestcase:
    def __init__(
        self,
        source_path,
        test_script_path,
        cmd,
        verbose=False
    ):
        '''
        :param verbose: if True, print extra debug information to help understand
                    why a test is not working
        '''
        self.prompt = '\(gdb\) '
        self.source_path = source_path
        import pexpect
        self.child = pexpect.spawn(
            cmd[0],
            cmd[1:],
            encoding='utf-8'
        )
        if verbose:
            self.child.logfile = sys.stdout
        self.child.setecho(False)
        self.child.waitnoecho()
        self.child.expect(self.prompt)
        test = imp.load_source('test', test_script_path)
        exception = None
        try:
            test.test(self)
        except AssertionError as e:
            exception = e
        self.child.sendcontrol('d')
        self.child.close()
        if exception is not None:
            raise exception

    def before(self):
        return self.child.before.rstrip()

    def continue_to(self, lineid):
        line_number = self.find_line(lineid)
        self.sendline('tbreak {}'.format(line_number))
        self.sendline('continue')

    def get_int(self, int_id):
        self.sendline('printf "%d\\n", {}'.format(int_id))
        return int(self.before())

    def get_float(self, float_id):
        self.sendline('printf "%f\\n", {}'.format(float_id))
        return float(self.before())

    def find_line(self, lineid):
        '''
        Search for the first line that contains a comment line
        that ends in /* test-gdb-<lineid> */ and return the line number.
        '''
        lineend = '/* test-gdb-' + lineid + ' */'
        with open(self.source_path, 'r') as f:
            for i, line in enumerate(f):
                if line.rstrip().endswith(lineend):
                    return i + 1
        return -1

    def sendline(self, line):
        self.child.sendline(line)
        self.child.expect(self.prompt)

class Main(common.LkmcCliFunction):
    def __init__(self):
        super().__init__(description='''\
Connect with GDB to an emulator to debug Linux itself
''')
        self.add_argument(
            '--after', default='',
            help='Pass extra arguments to GDB, to be appended after all other arguments'
        )
        self.add_argument(
            '--before', default='',
            help='Pass extra arguments to GDB to be prepended before any of the arguments passed by this script'
        )
        self.add_argument(
            'break_at', nargs='?',
            help='Extra options to append at the end of the emulator command line'
        )
        self.add_argument(
            '-k', '--kgdb', default=False,
        )
        self.add_argument(
            '-C', '--no-continue', default=False,
            help="Don't run continue after connecting"
        )
        self.add_argument(
            '-X', '--no-lxsymbols', default=False,
        )
        self.add_argument(
            '--test', default=False,
            help='''\
Run an expect test case instead of interactive usage. For baremetal and userland,
the script is a .py file next to the source code.
'''
        )
        self.add_argument(
            '--sim', default=False,
            help='''Use the built-in GDB CPU simulator
See: https://github.com/cirosantilli/linux-kernel-module-cheat#gdb-builtin-cpu-simulator
'''
        )
        self.add_argument(
            '-u', '--userland',
        )

    def timed_main(self):
        after = self.sh.shlex_split(self.env['after'])
        before = self.sh.shlex_split(self.env['before'])
        no_continue = self.env['no_continue']
        if self.env['test']:
            no_continue = True
            before.extend([
                '-q', LF,
                '-nh', LF,
                '-ex', 'set confirm off', LF
            ])
        elif self.env['verbose']:
            # The output of this would affect the tests.
            # https://stackoverflow.com/questions/13496389/gdb-remote-protocol-how-to-analyse-packets
            # Also be opinionated and set remotetimeout to allow you to step debug the emulator at the same time.
            before.extend([
                '-ex', 'set debug remote 1', LF,
                '-ex', 'set remotetimeout 99999', LF,
            ])
        if self.env['break_at'] is not None:
            break_at = ['-ex', 'break {}'.format(self.env['break_at']), LF]
        else:
            break_at = []
        linux_full_system = (self.env['baremetal'] is None and self.env['userland'] is None)
        if self.env['userland']:
            image = self.resolve_userland(self.env['userland'])
        elif self.env['baremetal']:
            image = self.env['image']
            test_script_path = os.path.splitext(self.env['source_path'])[0] + '.py'
        else:
            image = self.env['vmlinux']
        if self.env['baremetal']:
            allowed_toolchains = ['crosstool-ng', 'buildroot', 'host']
        else:
            allowed_toolchains = ['buildroot', 'crosstool-ng', 'host']
        cmd = (
            [self.get_toolchain_tool('gdb', allowed_toolchains=allowed_toolchains), LF] +
            before
        )
        if linux_full_system:
            cmd.extend(['-ex', 'add-auto-load-safe-path {}'.format(self.env['linux_build_dir']), LF])
        if self.env['sim']:
            target = 'sim'
        else:
            if self.env['kgdb']:
                port = self.env['extra_serial_port']
            else:
                port = self.env['gdb_port']
            target = 'remote localhost:{}'.format(port)
        cmd.extend([
            '-ex', 'file {}'.format(image), LF,
            '-ex', 'target {}'.format(target), LF,
        ])
        if not self.env['kgdb']:
            cmd.extend(break_at)
        if not no_continue:
            # ## lx-symbols
            #
            # ### lx-symbols after continue
            #
            # lx symbols must be run after continue.
            #
            # running it immediately after the connect on the bootloader leads to failure,
            # likely because kernel structure on which it depends are not yet available.
            #
            # With this setup, continue runs, and lx-symbols only runs when a break happens,
            # either by hitting the breakpoint, or by entering Ctrl + C.
            #
            # Sure, if the user sets a break on a raw address of the bootloader,
            # problems will still arise, but let's think about that some other time.
            #
            # ### lx-symbols autoload
            #
            # The lx-symbols commands gets loaded through the file vmlinux-gdb.py
            # which gets put on the kernel build root when python debugging scripts are enabled.
            cmd.extend(['-ex', 'continue', LF])
            if not self.env['no_lxsymbols'] and linux_full_system:
                cmd.extend(['-ex', 'lx-symbols {}'.format(self.env['kernel_modules_build_subdir']), LF])
        cmd.extend(after)
        if self.env['test']:
            self.sh.print_cmd(cmd)
            if not self.env['dry_run']:
                GdbTestcase(
                    self.env['source_path'],
                    test_script_path,
                    self.sh.strip_newlines(cmd),
                    verbose=self.env['verbose'],
                )
        else:
            # I would rather have cwd be out_rootfs_overlay_dir,
            # but then lx-symbols cannot fine the vmlinux and fails with:
            # vmlinux: No such file or directory.
            return self.sh.run_cmd(
                cmd,
                cmd_file=os.path.join(self.env['run_dir'], 'run-gdb.sh'),
                cwd=self.env['linux_build_dir']
            )

if __name__ == '__main__':
    Main().cli()
